<?php

/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * This file contains the base classes and helper functions for PDODB.
 *
 * This file contains the following classes and helper functions:
 *
 * <ul>
 * <li>PDODB<li>
 * <li>PDODB_Statement</li>
 * <li>PDODB_Module</li>
 * </ul>
 *
 * PHP version 5
 *
 * This file is part of PEAR::PDODB - A database abstraction layer for PHP5 built
 * using PDO. PHP version 5.1+ is recommended.
 *
 * PEAR::PDODB is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option)
 * any later version.
 *
 * PEAR::PDODB is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with PEAR::PDODB. If not, see <http://www.gnu.org/licenses/>.
 *
 * @package     PDODB
 * @category    Database
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 * @copyright   2007 Michael J. I. Jackson
 * @license     http://www.gnu.org/licenses/lgpl-3.0.txt GNU LGPL 3.0
 * @version     SVN: $Id: PDODB.php 15 2008-02-02 21:24:04Z mjijackson $
 * @link        http://pear.php.net/packages/PDODB
 */

/**
 * Require PDODB_Exception
 */
require_once str_replace('.php', '', __FILE__) . DIRECTORY_SEPARATOR . 'Exception.php';

/**
 * A connection manager/generator class for PDODB database connections.
 *
 * @package     PDODB
 * @category    Database
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 * @final
 */
final class PDODB
{

    /**
     * The PDODB API version number.
     *
     * @var     string
     * @access  private
     * @static
     */
    private static $api_version = '0.1';

    /**
     * Contains all instances of PDODB connections. Used to serve the SAME
     * connection object when {@link singleton()} is called.
     *
     * @var     array
     * @access  private
     * @static
     */
    private static $instances = array();

    /**#@+
     * PDODB error constants. Used by the various drivers to standardize
     * error codes across DBMS.
     *
     * @var     int
     * @access  public
     */
    const E_UNKNOWN             = -1;
    const E_SYNTAX              = -2;
    const E_NOT_FOUND           = -3;
    const E_ALREADY_EXISTS      = -4;
    const E_TRUNCATED           = -5;
    const E_INVALID             = -6;
    const E_INVALID_NUMBER      = -7;
    const E_DIVZERO             = -8;
    const E_CANNOT_CREATE       = -9;
    const E_CANNOT_DELETE       = -10;
    const E_CANNOT_DROP         = -11;
    const E_CANNOT_ALTER        = -12;
    const E_CANNOT_REPLACE      = -13;
    const E_NODBSELECTED        = -14;
    const E_NOSUCHDB            = -15;
    const E_NOSUCHTABLE         = -16;
    const E_NOSUCHCOLUMN        = -17;
    const E_NOT_LOCKED          = -18;
    const E_ACCESS_VIOLATION    = -19;
    const E_CONSTRAINT          = -20;
    const E_CONSTRAINT_NOT_NULL = -21;
    const E_DEADLOCK            = -22;
    /**#@-*/

    /**#@+
     * Driver attribute constants derived from PDO. These are mapped to
     * PDODB constants of the same name so that users can always use the
     * PDODB::ATTR_* constant without needing to remember which attribute
     * constants are found in PDO and which are found in PDODB.
     *
     * @var     int
     * @access  public
     */
    const ATTR_AUTOCOMMIT               = PDO::ATTR_AUTOCOMMIT;
    const ATTR_PREFETCH                 = PDO::ATTR_PREFETCH;
    const ATTR_TIMEOUT                  = PDO::ATTR_TIMEOUT;
    const ATTR_ERRMODE                  = PDO::ATTR_ERRMODE;
    const ATTR_SERVER_VERSION           = PDO::ATTR_SERVER_VERSION;
    const ATTR_CLIENT_VERSION           = PDO::ATTR_CLIENT_VERSION;
    const ATTR_SERVER_INFO              = PDO::ATTR_SERVER_INFO;
    const ATTR_CONNECTION_STATUS        = PDO::ATTR_CONNECTION_STATUS;
    const ATTR_CASE                     = PDO::ATTR_CASE;
    const ATTR_CURSOR_NAME              = PDO::ATTR_CURSOR_NAME;
    const ATTR_CURSOR                   = PDO::ATTR_CURSOR;
    const ATTR_ORACLE_NULLS             = PDO::ATTR_ORACLE_NULLS;
    const ATTR_PERSISTENT               = PDO::ATTR_PERSISTENT;
    const ATTR_STATEMENT_CLASS          = PDO::ATTR_STATEMENT_CLASS;
    const ATTR_FETCH_TABLE_NAMES        = PDO::ATTR_FETCH_TABLE_NAMES;
    const ATTR_FETCH_CATALOG_NAMES      = PDO::ATTR_FETCH_CATALOG_NAMES;
    const ATTR_DRIVER_NAME              = PDO::ATTR_DRIVER_NAME;
    const ATTR_STRINGIFY_FETCHES        = PDO::ATTR_STRINGIFY_FETCHES;
    const ATTR_MAX_COLUMN_LEN           = PDO::ATTR_MAX_COLUMN_LEN;
    const ATTR_EMULATE_PREPARES         = PDO::ATTR_EMULATE_PREPARES;
    const ATTR_DEFAULT_FETCH_MODE       = PDO::ATTR_DEFAULT_FETCH_MODE;
    /**#@-*/

    /**#@+
     * PDODB connection attribute constants.
     *
     * @var     int
     * @access  public
     */
    const ATTR_FETCH_CLASS              = 100;
    const ATTR_SILENT_WARNINGS          = 101;
    const ATTR_IDXNAME_FORMAT           = 102;
    const ATTR_SEQNAME_FORMAT           = 103;
    const ATTR_SEQCOL_NAME              = 104;
    const ATTR_DEFAULT_TABLE_TYPE       = 105;
    const ATTR_DEFAULT_DECIMAL_PLACES   = 106;
    const ATTR_DEFAULT_TEXT_LENGTH      = 107;
    const ATTR_QUOTE_IDENTIFIERS        = 108;
    const ATTR_EVENT_LISTENER           = 109;
    const ATTR_MODULES                  = 110;
    /**#@-*/

    /**
     * Gets the PDODB API version number.
     *
     * @return  string      The PDODB API version number
     * @access  public
     * @static
     */
    public static function apiVersion()
    {
        return self::$api_version;
    }

    /**
     * Generates a database connection object with the specified parameters. May
     * be used in two ways. For lazy connecting, use a PDO DSN string as the
     * first parameter:
     *
     * <code>
     * $options = array(
     *     PDO::ATTR_CASE               => PDO::CASE_UPPER,
     *     PDODB::ATTR_FETCH_CLASS      => 'MyDefaultFetchClass',
     *     PDODB::SILENT_WARNINGS       => true
     * );
     * try {
     *     $db = PDODB::factory('mysql:host=localhost;dbname=mydb',
     *         'root', 'password', $options);
     * } catch (PDODB_Exception $e) {
     *     print $e; // catch PDODB exceptions
     * }
     * </code>
     *
     * For already established database connections, a PDO object may be
     * passed directly to the constructor:
     *
     * <code>
     * $options = array(
     *     PDO::ATTR_CASE               => PDO::CASE_UPPER, // PDODB::ATTR_CASE also works
     *     PDODB::ATTR_EVENT_LISTENER   => new MyEventListener() // class MyEventListener either
     *                                                           // implements PDODB_Listener or
     *                                                           // extends PDODB_Listener_Default
     * );
     * try {
     *     $pdo = new PDO('mysql:host=localhost;dbname=mydb', 'root', 'password');
     *     $db = PDODB::factory($pdo, $options);
     * } catch (PDOException $e) {
     *     print $e->getMessage(); // catch exceptions thrown by PDO
     * } catch (PDODB_Exception $e) {
     *     print $e; // catch exceptions thrown by PDODB
     * }
     * </code>
     *
     * When used in the second mode, the second parameter will be used as the
     * $options array. This array is an associative array of connection-specific
     * options to use for the connection. It may contain any of PDO's
     * {@link http://php.net/manual/ref.pdo.php#pdo.drivers driver-specific}
     * connection options, or any of the {@link PDODB_Connection::$options PDODB options}.
     * Also, because all PDO options are mapped to PDODB options of the same
     * name, the PDODB::ATTR_* family of constants may always be used for
     * consistency.
     *
     * Unless otherwise specified in the $options array, the following settings
     * will be used on all connections:
     *
     * <ul>
     * <li>PDO::ATTR_CASE => PDO::CASE_NATURAL</li>
     * <li>PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true</li>
     * </ul>
     *
     * Also, the default error mode for PDODB connections is PDO::ERRMODE_EXCEPTION
     * for consistency in error handling. This value may not be changed.
     *
     * @param   mixed   $dsn            The data source name to use or a PDO
     *                                  instance
     * @param   array   $options        Some connection-specific options to use
     * @return  PDODB_Connection        The database connection object
     * @throws  PDODB_UnsupportedDriverException    If the specified driver is
     *                                              not yet supported in PDODB
     * @access  public
     * @static
     */
    public static function factory($dsn, $user = null, $pass = null,
        $options = array())
    {
        if (is_string($dsn)) {
            $driver = preg_replace('/:.*$/', '', $dsn);
            $dsn = array(
                'dsn'   => $dsn,
                'user'  => $user,
                'pass'  => $pass
            );
        } elseif ($dsn instanceof PDO) {
            $driver = $dsn->getAttribute(PDO::ATTR_DRIVER_NAME);
            if (is_array($user)) {
                $options = $user; // 2nd param is options
            }
        }

        try {
            // load the requested driver class
            $driver_cls = "PDODB_Connection_$driver";
            PDODB::loadClass($driver_cls);
        } catch (PDODB_Exception $e) {
            throw new PDODB_UnsupportedDriverException($driver, $e);
        }

        $db = new $driver_cls($dsn, $options, count(self::$instances));

        // throw exceptions
        $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

        // use the PDODB_Statement class unless otherwise specified
        if (!array_key_exists(PDO::ATTR_STATEMENT_CLASS, $options)) {
            $db->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('PDODB_Statement',
                array($db->getId())));
        }

        // use natural case unless otherwise specified
        if (!array_key_exists(PDO::ATTR_CASE, $options)) {
            $db->setAttribute(PDO::ATTR_CASE, PDO::CASE_NATURAL);
        }

        // use buffered queries in MySQL unless otherwise specified
        if ($driver == 'mysql'
            && !array_key_exists(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, $options)) {
                $db->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true);
        }

        self::$instances[] = array(
            'conn'      => $db,
            'dsn'       => $dsn,
            'user'      => $user
        );

        return $db;
    }

    /**
     * Gets a database connection object with the specified parameters. If an
     * object with the same DSN, username, and password already exists, it will
     * be returned.
     *
     * @param   string  $dsn            The PDO data source name to use
     * @param   string  $user           The user name to use
     * @param   string  $pass           The password to use
     * @param   array   $options        Some connection-specific options to use
     * @return  PDODB_Connection        The database connection object
     * @access  public
     * @static
     */
    public static function singleton($dsn, $user = null, $pass = null,
        $options = array())
    {
        foreach (self::$instances as $inst) {
            if ($inst['dsn'] != $dsn) {
                continue;
            }
            if ($user == $inst['user']) {
                $conn = $inst['conn'];

                // apply options
                foreach ($options as $attr => $value) {
                    $conn->setAttribute($attr, $value);
                }

                return $conn;
            }
        }

        return PDODB::factory($dsn, $user, $pass, $options);
    }

    /**
     * Gets the database connection instance with the specified connection id.
     * Most of the time, this function should not be called manually. It exists
     * primarily for the sake of the statement and module classes and is used by
     * them internally to retrieve the connection instances associated with them.
     *
     * @param   int                 $conn_id        The connection id
     * @return  PDODB_Connection                    The connection at the
     *                                              specified index
     * @throws  PDODB_InstanceNotFoundException     If the instance can't be
     *                                              found (should never happen)
     * @access  public
     * @static
     */
    public static function getConnection($conn_id)
    {
        if (isset(self::$instances[$conn_id])) {
            return self::$instances[$conn_id]['conn'];
        }

        throw new PDODB_InstanceNotFoundException($conn_id);
    }

    /**
     * Returns a list of available PDODB drivers. Available drivers include all
     * PDODB sub-packages that have been installed on this machine.
     *
     * @return  array       An array containing all available driver names
     * @access  public
     * @static
     */
    public static function getAvailableDrivers()
    {
        $drivers = array();
        foreach (new DirectoryIterator(str_replace('.php', '', __FILE__)
            . DIRECTORY_SEPARATOR . 'Connection') as $file) {
                if ($file->isFile()) {
                    $drivers[] = str_replace('.php', '', $file->getFilename());
                }
        }

        return $drivers;
    }

    /**
     * Determines if the given driver is installed on this machine. In order for
     * the driver to be installed, both the PDO extension and the corresponding
     * PDODB sub-package must be installed. Usage:
     *
     * <code>
     * if (PDODB::driverInstalled('mysql')) {
     *     ... // instantiate MySQL driver
     * } else {
     *     echo 'No MySQL available';
     * }
     * </code>
     *
     * @param   string  $driver     The name of the driver
     * @return  bool                True if the driver is installed, false
     *                              otherwise
     * @access  public
     * @static
     */
    public static function driverInstalled($driver)
    {
        $driver = strtolower($driver);
        return in_array($driver, PDO::getAvailableDrivers())
            && in_array($driver, PDODB::getAvailableDrivers());
    }

    /**
     * Loads a PDODB class.
     *
     * @param   string  $class_name     The class name to load
     * @return  void
     * @throws  PDODB_FileNotFoundException     If the corresponding class file
     *                                          isn't found in the PDODB package
     * @throws  PDODB_ClassNotFoundException    If the class cannot be found
     * @access  public
     * @static
     */
    public static function loadClass($class_name)
    {
        if (class_exists($class_name, false)) {
            return;
        }

        $class_file = dirname(__FILE__) . DIRECTORY_SEPARATOR
            . str_replace('_', DIRECTORY_SEPARATOR, $class_name) . '.php';
        if (!is_file($class_file)) {
            throw new PDODB_FileNotFoundException($class_file);
        }

        require_once $class_file;

        if (!class_exists($class_name, false)) {
            throw new PDODB_ClassNotFoundException($class_name, $class_file);
        }
    }

}

/**
 * A wrapper class for PDOStatement objects. Provides some convenient methods
 * for extracting data.
 *
 * @package     PDODB
 * @category    Database
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 */
class PDODB_Statement extends PDOStatement
{

    /**
     * The global instance index of this statement's connection instance. Used
     * to retrieve this statement's connection using
     * {@link PDODB::getConnection()}.
     *
     * @var     int
     * @access  protected
     */
    protected $conn_id;

    /**
     * Constructor.
     *
     * @param   int     $conn_id        The index of this statement's connection
     *                                  instance in PDODB::$instances
     * @access  protected
     */
    protected function __construct($conn_id)
    {
        $this->conn_id = $conn_id;
    }

    /**
     * Gets the connection associated with this statement.
     *
     * @return  PDODB_Connection        The connection that generated this
     *                                  statement
     * @access  public
     */
    public function getConnection()
    {
        return PDODB::getConnection($this->conn_id);
    }

    /**
     * Executes this statement. A wrapper function for
     * {@link http://php.net/manual/function.PDOStatement-execute.php PDOStatement::execute()}.
     *
     * @param   array   $input_parameters       The parameters to use in the
     *                                          prepared statement
     * @return  bool                            True on success, false on failure
     * @throws  PDODB_ConnectionException       If query execution fails
     * @access  public
     */
    public function execute($input_parameters = array())
    {
        try {
            return parent::execute($input_parameters);
        } catch (PDOException $e) {
            $code = $this->getConnection()->errorCode($e);
            throw PDODB_ConnectionException::factory($code, $e, $this->queryString);
        }
    }

    /**
     * Performs multiple calls to {@link execute()} with this statement. Each
     * array element of $input_parameters should itself be another array
     * suitable to pass to {@link execute()}.
     *
     * @param   array   $input_parameters       An array of input parameters
     * @return  void
     * @throws  PDODB_ConnectionException       If any query execution fails
     * @access  public
     * @see     execute()
     */
    public function executeMultiple($input_parameters = array())
    {
        foreach ($input_parameters as $ip) {
            $this->execute($ip);
        }
    }

    /**
     * Fetches the result of the query as an array of objects. If no class is
     * specified, the fetch_class option of this statement's connection will be
     * used.
     *
     * @return  array           An array of objects resulting from the query
     * @access  public
     */
    public function fetchObjects($class_name = null, $ctor_args = array())
    {
        if (is_null($class_name)) {
            $db = $this->getConnection();
            $class_name = $db->getAttribute(PDODB::ATTR_FETCH_CLASS);
        }

        return $this->fetchAll(PDO::FETCH_CLASS, $class_name, $ctor_args);
    }

    /**
     * Fetches the next row of the result as an object. If no class is given,
     * the fetch_class option of this statement's connection will be used.
     * Otherwise, this function behaves the same as
     * {@link http://www.php.net/manual/function.PDOStatement-fetchObject.php PDOStatement::fetchObject()}.
     *
     * @return  object          An object from the next row of the result
     * @access  public
     */
    public function fetchObject($class_name = null, $ctor_args = array())
    {
        if (is_null($class_name)) {
            $db = $this->getConnection();
            $class_name = $db->getAttribute(PDODB::ATTR_FETCH_CLASS);
        }

        return parent::fetchObject($class_name, $ctor_args);
    }

    /**
     * Fetches the result of the query as an array of arrays.
     *
     * @return  array           An array of arrays resulting from the query
     * @access  public
     */
    public function fetchRows()
    {
        return $this->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * Fetches the next row (or the specified row) of the result as an array.
     *
     * @return  array           An array representing the specified row of the
     *                          query result
     * @access  public
     */
    public function fetchRow()
    {
        return $this->fetch(PDO::FETCH_ASSOC);
    }

    /**
     * Gets the number of rows in the result set of a SELECT statement.
     *
     * @return  int             The number of rows in the result set
     * @access  public
     */
    public function numRows()
    {
        return count($this->fetchAll(PDO::FETCH_COLUMN, 0));
    }

    /**
     * An alias for
     * {@link http://php.net/manual/function.PDOStatement-closeCursor.php PDOStatement::closeCursor()}.
     *
     * @return  bool            True on success, false on failure
     * @access  public
     */
    public function free()
    {
        return $this->closeCursor();
    }

}

/**
 * A base class for PDODB modules.
 *
 * @package     PDODB
 * @category    Database
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 */
abstract class PDODB_Module
{

    /**
     * The global instance index of this module's connection. Used to retrieve
     * this module's connection instance using {@link PDODB::getConnection()}.
     *
     * @var     int
     * @access  protected
     */
    private $conn_id;

    /**
     * Constructor.
     *
     * @param   int     $conn_id        The index of this module's connection
     *                                  instance in {@link PDODB::$instances}
     * @access  public
     */
    public function __construct($conn_id)
    {
        $this->conn_id = $conn_id;
    }

    /**
     * Gets the connection for this module.
     *
     * @return  PDODB_Connection        The connection instance for this module
     * @access  public
     */
    public function getConnection()
    {
        return PDODB::getConnection($this->conn_id);
    }

}

?>
